﻿using Arch.Data;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.ServiceModel.DomainServices.Server;

namespace Arch.DomainServices
{
    public class EntityDomainOperationEntry : DomainOperationEntry
    {
        private MethodInfo _method;

        public EntityDomainOperationEntry(Type domainServiceType, Type entityType, DomainOperation operation, AttributeCollection attributes)
            : base(
            domainServiceType,
            GetMethodName(entityType, operation),
            operation,
            GetMethodReturnType(entityType, operation),
            GetMethodParameters(entityType, operation),
            attributes)
        {
            switch (this.Operation)
            {
                case DomainOperation.Query:
                case DomainOperation.Insert:
                case DomainOperation.Update:
                case DomainOperation.Delete:
                    break;
                default:
                    throw new ArgumentException(
                        String.Format(
                            CultureInfo.InvariantCulture,
                            "Operation type '{0}' not supported",
                            this.Operation),
                            "operation");
            }

            this.FindMethod();
        }

        public override object Invoke(DomainService domainService, object[] parameters)
        {
            var service = (RepositoryDomainService)domainService;

            if (Operation == DomainOperation.Update || Operation == DomainOperation.Delete)
            {
                if (parameters == null || !parameters.Any())
                {
                    throw new ArgumentException("Invalid number of parameters for Update domain operation.");
                }

                return this._method.Invoke(this, new object[] { service, parameters[0] });
            }
            else
            {
                return this._method.Invoke(service.Repository, parameters);
            }
        }

        internal static MethodInfo GetSpecificMethod(Type domainServiceType, Type entityType, DomainOperation operation)
        {
            var name = operation.ToString().Replace("Query", "Get").Replace("Insert", "Create") + entityType.Name;
            var argTypes = operation == DomainOperation.Query ? new Type[0] : new Type[] { entityType };
            var modifier = new ParameterModifier[] { };
            var method = domainServiceType.GetMethod(
                                            name,
                                            BindingFlags.Public | BindingFlags.Instance | BindingFlags.ExactBinding,
                                            null,
                                            argTypes,
                                            modifier);
            return method;
        }

        private static void Update<TEntity>(RepositoryDomainService domainService, TEntity entity) where TEntity : class, new()
        {
            domainService.Repository.Attach(entity, true);
            domainService.Repository.Update(entity);
        }

        private static void Delete<TEntity>(RepositoryDomainService domainService, TEntity entity) where TEntity : class, new()
        {
            domainService.Repository.Attach(entity);
            domainService.Repository.Delete(entity);
        }

        private static string GetMethodName(Type entityType, DomainOperation operation)
        {
            string suffix = entityType.Name;
            return operation.ToString().Replace("Query", "Get").Replace("Insert", "Create") + suffix;
        }

        private static Type GetMethodReturnType(Type entityType, DomainOperation operation)
        {
            return operation == DomainOperation.Query ? typeof(IQueryable<>).MakeGenericType(entityType) : typeof(void);
        }

        private static List<DomainOperationParameter> GetMethodParameters(Type entityType, DomainOperation operation)
        {
            return operation == DomainOperation.Query ?
                        new List<DomainOperationParameter>() :
                        new List<DomainOperationParameter>
                        {
                            new DomainOperationParameter(
                                Char.ToLower(entityType.Name[0], CultureInfo.InvariantCulture) + entityType.Name.Remove(0, 1), 
                                entityType,
                                new AttributeCollection())
                        };
        }

        private void FindMethod()
        {
            // Specific method
            this._method = GetSpecificMethod(this.DomainServiceType, this.AssociatedType, this.Operation);

            // Generic method
            if (this._method == null)
            {
                Type methodType;
                Type[] argTypes;
                switch (Operation)
                {
                    case DomainOperation.Query:
                        methodType = typeof(IRepository);
                        argTypes = new Type[0];
                        break;
                    case DomainOperation.Delete:
                    case DomainOperation.Update:
                        methodType = typeof(EntityDomainOperationEntry);
                        argTypes = new Type[] { typeof(RepositoryDomainService), this.AssociatedType };
                        break;
                    case DomainOperation.Insert:
                    default:
                        methodType = typeof(IRepository);
                        argTypes = new Type[] { this.AssociatedType };
                        break;
                }

                var methodName = Operation.ToString().Replace("Query", "GetQuery").Replace("Insert", "Create");
                this._method = methodType.GetMethod(
                                            methodName,
                                            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static,
                                            new DomainMethodBinder(),
                                            argTypes,
                                            new ParameterModifier[] { });
                if (this._method == null)
                {
                    throw new NotImplementedException("No matching signature found.");
                }

                this._method = this._method.MakeGenericMethod(this.AssociatedType);
            }
        }

        private class DomainMethodBinder : Binder
        {
            public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers)
            {
                var query = from m in match
                            where m.IsGenericMethod
                            select m;
                var list = query.ToList();
                if (list.Count != 1)
                {
                    throw new AmbiguousMatchException("Multiple signatures founds.");
                }

                return list[0];
            }

            public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }

            public override MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] names, out object state)
            {
                throw new NotImplementedException();
            }

            public override object ChangeType(object value, Type type, System.Globalization.CultureInfo culture)
            {
                throw new NotImplementedException();
            }

            public override void ReorderArgumentArray(ref object[] args, object state)
            {
                throw new NotImplementedException();
            }

            public override PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers)
            {
                throw new NotImplementedException();
            }
        }
    }
}
